package com.ouc.pampas;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import io.terminus.common.exception.ServiceException;
import io.terminus.pampas.client.ParamUtil;
import io.terminus.pampas.engine.mapping.ParamConverter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class SpringExecutor extends Executor<Object> {
	private static final Logger log = LoggerFactory.getLogger(SpringExecutor.class);
	private static final Splitter splitter = Splitter.on(':').trimResults().omitEmptyStrings();
	private final LoadingCache<String, ParamUtil.MethodInfo> methodInfos;

	protected static class ServiceInfo {
		private final Class<?> klass;
		private final Method method;
		private final Class<?>[] types;
		private final String[] paramNames;

		public ServiceInfo(Class<?> klass, Method method, Class<?>[] types, String[] paramNames) {
			this.klass = klass;
			this.method = method;
			this.types = types;
			this.paramNames = paramNames;
		}

		public Class<?> getKlass() {
			return this.klass;
		}

		public Method getMethod() {
			return this.method;
		}

		public Class<?>[] getTypes() {
			return this.types;
		}

		public String[] getParamNames() {
			return this.paramNames;
		}
	}

	@Autowired
	private ApplicationContext applicationContext;

	public SpringExecutor() {
		CacheLoader<String, ParamUtil.MethodInfo> loader = new CacheLoader() {
			@Override
			public Object load(Object key) throws Exception {
				List<String> parts = SpringExecutor.splitter.splitToList((CharSequence) key);
				if (parts.size() != 2) {
					throw new IllegalArgumentException(
							"bad api format,should be interfaceName:methodName,but is: " + key);
				}
				Class<?> klass = Class.forName((String) parts.get(0));
				Object bean = SpringExecutor.this.applicationContext.getBean(klass);
				Method method = SpringExecutor.this.findMethodByName(klass, (String) parts.get(1));
				if (method == null) {
					throw new NoSuchMethodException("failed to find method: " + key);
				}
				return ParamUtil.getMethodInfo(bean, method);
			}
		};
		this.methodInfos = CacheBuilder.newBuilder().build(loader);
	}

	public Object exec(Service service, Map<String, Object> params) {
		String api = service.getUri();
		if (Strings.isNullOrEmpty(api)) {
			return null;
		}
		ParamUtil.MethodInfo methodInfo = (ParamUtil.MethodInfo) this.methodInfos.getUnchecked(api);

		LinkedHashMap<String, ParamUtil.ParamInfo> paramsInfo = methodInfo.getParams();
		Object[] concernedParams = new Object[paramsInfo.size()];
		int index = 0;
		for (String paramName : paramsInfo.keySet()) {
			ParamUtil.ParamInfo paramInfo = (ParamUtil.ParamInfo) paramsInfo.get(paramName);

			Object param = ParamConverter.convertParam(paramName, paramInfo.getClazz(), params);

			concernedParams[(index++)] = ParamUtil.convert(param, paramInfo.getClazz(), paramInfo.getJavaType(),
					params);
		}
		Object object;
		try {
			object = methodInfo.getMethod().invoke(methodInfo.getBean(), concernedParams);
		} catch (IllegalAccessException e) {
			log.error("illegal access method, service: {}", service, e);
			throw new ServiceException(e);
		} catch (InvocationTargetException e) {
			log.error("invocation target exception, service: {}", service, e);
			if ((e.getTargetException() instanceof RuntimeException)) {
				throw ((RuntimeException) e.getTargetException());
			}
			throw new ServiceException(e.getTargetException().getMessage(), e);
		}
		return unwrapResponse(object);
	}

	private Method findMethodByName(Class<?> beanClazz, String methodName) {
		Method[] methods = beanClazz.getMethods();
		for (Method method : methods) {
			if (method.getName().equals(methodName)) {
				return method;
			}
		}
		return null;
	}
}
